Introduce API Tokens with cluster_permissions and index_permissions directly associated with the token#5443
Conversation
…4921) Signed-off-by: Derek Ho <dxho@amazon.com>
…ect#4967) Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
…00 tokens outstanding (opensearch-project#5147) Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
DarshitChanpura
left a comment
There was a problem hiding this comment.
Thank you @cwperks for taking this over. Left some comments around testing and general usage.
src/main/java/org/opensearch/security/action/apitokens/ApiToken.java
Outdated
Show resolved
Hide resolved
src/main/java/org/opensearch/security/action/apitokens/ApiToken.java
Outdated
Show resolved
Hide resolved
src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java
Outdated
Show resolved
Hide resolved
src/main/java/org/opensearch/security/action/apitokens/ApiToken.java
Outdated
Show resolved
Hide resolved
| private final List<TokenListener> tokenListener = new ArrayList<>(); | ||
| private static final Logger log = LogManager.getLogger(ApiTokenRepository.class); | ||
|
|
||
| private final Map<String, RoleV7> jtis = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
+1. It might not be straightforward since JTIs seem to be populated on getTokenMetadata request and there doesn't seem to be a way to pass flattenedActionGroups to that call. I may be seeing the complete picture here, but something like updateJTIs(FlattenedAGs) from PrivilegeEvaluator to update jtis and then use that to populate pluginIdToActionPrivileges which will then be used to create PrivilegeEvalContext?
src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java
Show resolved
Hide resolved
| public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { | ||
| xContentBuilder.startObject(); | ||
| xContentBuilder.field("enabled", enabled); | ||
| xContentBuilder.field("signing_key", signing_key); |
There was a problem hiding this comment.
i think it fails in ApiTokenAuthenticator if it is anything lower than 512 bits.
src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java
Outdated
Show resolved
Hide resolved
src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java
Show resolved
Hide resolved
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Hi @cwperks ! First I wanted to thank you for your work on this PR. Really appreciate effort you put it in to move forward with this feature! Just wanted to ask whether is there any plan to soon merge your changes? Is there any blocking issues/ any help you would need? It would be really cool to have API tokens feature working :) |
|
I'll resolve the conflicts ASAP and try to push this forward for V1. Its important to know that this PR will still have a lot of limitations, but paves the way for expansion. For instance, in this PR only the admin can issue tokens. |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
DarshitChanpura
left a comment
There was a problem hiding this comment.
thank you for picking this up @cwperks . Left a few comments. Main comment is addition of e2e test which checks authn+authz with API token,
src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java
Outdated
Show resolved
Hide resolved
src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java
Outdated
Show resolved
Hide resolved
| updateRequest, | ||
| ActionListener.wrap( | ||
| updateResponse -> listener.onResponse(response), | ||
| exception -> listener.onFailure(new ApiTokenException("Failed to refresh cache", exception)) |
There was a problem hiding this comment.
this message should be "Failed to update API token"
src/main/java/org/opensearch/security/identity/SecurityTokenManager.java
Outdated
Show resolved
Hide resolved
src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java
Show resolved
Hide resolved
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
…bled Signed-off-by: Craig Perkins <cwperx@amazon.com>
…tion a duration and not epoch Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Description
Re-basing #5225 with the latest changes from
main.This PR introduces API Tokens — a new capability in the Security plugin that allows security admins to issue long-lived, scoped tokens and associate permissions directly with the token.
How it works
API Tokens are opaque tokens with the format
os_<random>. When a token is created, a SHA-256 hash of the plaintext token is stored in a system index,.opensearch_security_api_tokens. The plaintext token is returned once at creation time and never stored. On each request, the incoming token is hashed and looked up in an in-memory cache populated from the index.Tokens are authenticated via the
Authorization: ApiKey <token>header.What is novel about this approach compared to OBO tokens is that permissions are scoped directly to the token rather than derived from the issuing user's roles. An admin can issue a token with only the permissions it needs — for example, read-only access to a single index — regardless of the admin's own permissions. This enforces the principle of least privilege and is a key building block toward deprecating Roles Injection, the current practice for how plugins run async jobs with user-scoped permissions.
Revocation model
Tokens use a soft-delete revocation model. When a token is revoked via
DELETE /_plugins/_security/api/apitokens/{id}, the document in the index is updated with arevoked_attimestamp rather than being deleted. This means:revoked_atfield, enabling UIs to display revocation history and audit trails.revoked_atset are excluded from the in-memory authentication maps, so they cannot be used to authenticate.API Reference
Create API Token
POST /_plugins/_security/api/apitokensRequest:
{ "name": "my-token", "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [ { "index_pattern": ["logs-*"], "allowed_actions": ["indices:data/read/search"] } ], "expiration": 1800000 }Response:
{ "id": "Nd_pMRWeAC93ZGMhRa5CxX", "token": "os_abc123..." }The
idis used to manage the token, such as listing or revoking it. The plaintext token is returned once and never stored — save it immediately.List API Tokens
GET /_plugins/_security/api/apitokensReturns all tokens, including revoked ones. Revoked tokens include a
revoked_atfield (epoch millis).Response:
[ { "id": "Nd_pMRWeAC93ZGMhRa5CxX", "name": "my-token", "iat": 1742000000000, "expiration": 1800000, "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [ { "index_pattern": ["logs-*"], "allowed_actions": ["indices:data/read/search"] } ] }, { "id": "Xf_qNSZeBC04AHNiSb6DyY", "name": "old-token", "iat": 1741000000000, "expiration": 1800000, "revoked_at": 1741500000000, "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [] } ]Revoke API Token
DELETE /_plugins/_security/api/apitokens/{id}Response:
{ "message": "Token Nd_pMRWeAC93ZGMhRa5CxX revoked successfully." }Revocation is a soft-delete — the token metadata is retained with a
revoked_attimestamp. The token is immediately unusable after the response is returned. The cache refresh is broadcast synchronously to all nodes before the response is sent.Using a Token
Pass the token in the
Authorizationheader using theApiKeyscheme:Authorization: ApiKey os_abc123...Example — search a permitted index:
Response:
{ "hits": { "total": { "value": 3, "relation": "eq" }, "hits": [ ... ] } }Example — attempt a forbidden action:
Response:
{ "error": { "type": "security_exception", "reason": "no permissions for [indices:admin/delete]" }, "status": 403 }Issues Resolved
Partially resolves #4009, limited to security admins in the initial release.
Check List
--signoff